# =============================================================================
# scraper.py
# Módulo Python para scraping de tweets vía Playwright
# Proyecto: Corpus de tweets para investigación académica
# Versión: 2.0 — Sesión persistente (un solo navegador para todo el scraping)
# =============================================================================
#
# ARQUITECTURA v2:
#   A diferencia de v1 (que abría y cerraba Chrome en cada URL), esta versión
#   mantiene UNA SOLA sesión de navegador durante toda la ejecución.
#   Esto resuelve dos problemas:
#     1. Chrome no se abre/cierra 2,700+ veces
#     2. La autenticación se hace una sola vez y persiste
#
# USO DESDE R (via reticulate):
#   py_run_file("scraper.py")
#   py$iniciar_sesion(auth_token, config)   # Llamar UNA sola vez al inicio
#   tweets <- py$scrape_url_sesion(url, ids_vistos)  # Llamar por cada URL
#   py$cerrar_sesion()                      # Llamar al final
#
# NOTA SOBRE AUTH_TOKEN:
#   Para obtener tu auth_token:
#   1. Abre Chrome e inicia sesión en x.com
#   2. Instala Cookie-Editor:
#      https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm
#   3. Estando en x.com, abre Cookie-Editor y busca "auth_token"
#   4. Copia el valor
#   IMPORTANTE: No cierres sesión en X mientras corre el scraper.
# =============================================================================

import asyncio
import random
import threading
from playwright.async_api import async_playwright

# -----------------------------------------------------------------------------
# Estado global de la sesión persistente
# -----------------------------------------------------------------------------
_loop       = None   # Event loop de asyncio dedicado
_thread     = None   # Hilo donde corre el loop
_playwright = None   # Instancia de Playwright
_navegador  = None   # Navegador abierto
_contexto   = None   # Contexto de navegación (cookies, viewport, etc.)
_page       = None   # Página activa
_config     = None   # Configuración guardada


# -----------------------------------------------------------------------------
# FUNCIÓN: Extraer tweets visibles en la página actual
# -----------------------------------------------------------------------------
async def extraer_tweets_visibles(page):
    tweets = []
    articulos = await page.query_selector_all('article[data-testid="tweet"]')

    for articulo in articulos:
        try:
            tweet = {}

            # --- URL e ID ---
            link = await articulo.query_selector('a[href*="/status/"]')
            if link:
                href = await link.get_attribute("href")
                tweet["id"]  = href.split("/status/")[-1].split("?")[0] if "/status/" in (href or "") else ""
                tweet["url"] = f"https://x.com{href}" if href else ""
            else:
                tweet["id"]  = ""
                tweet["url"] = ""

            if not tweet["id"]:
                continue

            # --- Texto ---
            div_texto = await articulo.query_selector('div[data-testid="tweetText"]')
            tweet["texto"] = (await div_texto.inner_text()).strip() if div_texto else ""

            # --- Fecha ---
            time_el = await articulo.query_selector("time")
            tweet["fecha"] = await time_el.get_attribute("datetime") if time_el else ""

            # --- Métricas ---
            metricas = {
                "replies":  '[data-testid="reply"] span[data-testid="app-text-transition-container"]',
                "retweets": '[data-testid="retweet"] span[data-testid="app-text-transition-container"]',
                "likes":    '[data-testid="like"] span[data-testid="app-text-transition-container"]',
            }

            for nombre, selector in metricas.items():
                try:
                    el = await articulo.query_selector(selector)
                    if el:
                        val = (await el.inner_text()).strip().replace(",", "")
                        if "k" in val.lower():
                            tweet[nombre] = int(float(val.lower().replace("k", "")) * 1_000)
                        elif "m" in val.lower():
                            tweet[nombre] = int(float(val.lower().replace("m", "")) * 1_000_000)
                        else:
                            tweet[nombre] = int(val) if val.isdigit() else 0
                    else:
                        tweet[nombre] = 0
                except Exception:
                    tweet[nombre] = 0

            tweet["es_retweet"]   = tweet["texto"].startswith("RT @")
            tweet["es_respuesta"] = (not tweet["es_retweet"]) and tweet["texto"].startswith("@")

            tweets.append(tweet)

        except Exception:
            continue

    return tweets


# -----------------------------------------------------------------------------
# FUNCIÓN ASYNC: Iniciar sesión (abrir navegador y autenticar)
# -----------------------------------------------------------------------------
async def _iniciar_sesion_async(auth_token, config):
    global _playwright, _navegador, _contexto, _page, _config

    _config = config

    _playwright = await async_playwright().start()

    _navegador = await _playwright.chromium.launch(
        headless=config["modo_headless"],
        args=[
            "--no-sandbox",
            "--disable-blink-features=AutomationControlled",
        ]
    )

    _contexto = await _navegador.new_context(
        viewport={"width": 1280, "height": 900},
        user_agent=(
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/120.0.0.0 Safari/537.36"
        ),
        locale="es-MX",
        timezone_id="America/Mexico_City",
    )

    await _contexto.add_init_script(
        "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
    )

    # Inyectar cookies de autenticación
    await _contexto.add_cookies([
        {
            "name": "auth_token",
            "value": auth_token,
            "domain": ".x.com",
            "path": "/"
        },
        {
            "name": "guest_id",
            "value": "v1%3A000000000000000000",
            "domain": ".x.com",
            "path": "/"
        }
    ])

    _page = await _contexto.new_page()

    # Navegar a x.com para activar la sesión
    await _page.goto("https://x.com/home", wait_until="domcontentloaded",
                     timeout=config["timeout_pagina"])
    await asyncio.sleep(3)  # Esperar que cargue la sesión completamente


# -----------------------------------------------------------------------------
# FUNCIÓN ASYNC: Scrapear una URL en la sesión activa
# -----------------------------------------------------------------------------
async def _scrape_url_sesion_async(url, ids_vistos):
    global _page, _config

    todos_tweets = {}
    sin_cambio   = 0
    max_sin_cambio = 8

    # Navegar a la URL de búsqueda
    try:
        await _page.goto(url, wait_until="domcontentloaded",
                         timeout=_config["timeout_pagina"])
    except Exception:
        pass

    # Esperar que aparezcan tweets
    try:
        await _page.wait_for_selector(
            'article[data-testid="tweet"]',
            timeout=_config["timeout_elemento"]
        )
    except Exception:
        return []  # Página sin resultados

    # Ciclo de scroll y extracción
    while True:
        nuevos = await extraer_tweets_visibles(_page)
        antes  = len(todos_tweets)

        for t in nuevos:
            if t["id"] and t["id"] not in todos_tweets and t["id"] not in ids_vistos:
                todos_tweets[t["id"]] = t

        incorporados = len(todos_tweets) - antes

        if incorporados == 0:
            sin_cambio += 1
            if sin_cambio >= max_sin_cambio:
                break
        else:
            sin_cambio = 0

        await _page.evaluate(f"window.scrollBy(0, {_config['scroll_pixels']})")
        await asyncio.sleep(random.uniform(_config["delay_min"], _config["delay_max"]))

    return list(todos_tweets.values())


# -----------------------------------------------------------------------------
# FUNCIÓN ASYNC: Cerrar sesión
# -----------------------------------------------------------------------------
async def _cerrar_sesion_async():
    global _playwright, _navegador, _contexto, _page
    try:
        if _navegador:
            await _navegador.close()
        if _playwright:
            await _playwright.stop()
    except Exception:
        pass
    _navegador  = None
    _contexto   = None
    _page       = None
    _playwright = None


# -----------------------------------------------------------------------------
# GESTIÓN DEL EVENT LOOP EN HILO DEDICADO
# (necesario para llamadas desde R via reticulate)
# -----------------------------------------------------------------------------
def _arrancar_loop():
    global _loop
    _loop = asyncio.new_event_loop()
    asyncio.set_event_loop(_loop)
    _loop.run_forever()


def _ejecutar(coro):
    """Ejecuta una corrutina en el loop dedicado y devuelve el resultado."""
    future = asyncio.run_coroutine_threadsafe(coro, _loop)
    return future.result(timeout=300)  # Timeout de 5 minutos por operación


# -----------------------------------------------------------------------------
# API PÚBLICA — llamable desde R via reticulate
# -----------------------------------------------------------------------------
def iniciar_sesion(auth_token, config):
    """
    Abre el navegador y autentica la sesión en X.
    Llamar UNA SOLA VEZ al inicio del script R.
    
    Ejemplo desde R:
        py$iniciar_sesion(AUTH_TOKEN, CONFIG)
    """
    global _thread, _loop

    # Arrancar hilo con event loop si no existe
    if _thread is None or not _thread.is_alive():
        _thread = threading.Thread(target=_arrancar_loop, daemon=True)
        _thread.start()
        import time; time.sleep(0.5)  # Esperar que el loop arranque

    _ejecutar(_iniciar_sesion_async(auth_token, config))
    print("✓ Sesión iniciada en X")


def scrape_url_sesion(url, ids_vistos):
    """
    Scrapea una URL usando la sesión activa.
    Llamar por cada URL a procesar.
    
    Ejemplo desde R:
        tweets <- py$scrape_url_sesion(url, ids_vistos)
    """
    return _ejecutar(_scrape_url_sesion_async(url, set(ids_vistos)))


def cerrar_sesion():
    """
    Cierra el navegador y libera recursos.
    Llamar al final del script R.
    
    Ejemplo desde R:
        py$cerrar_sesion()
    """
    _ejecutar(_cerrar_sesion_async())
    print("✓ Sesión cerrada")


# -----------------------------------------------------------------------------
# COMPATIBILIDAD: mantener run_scraper para no romper scripts anteriores
# -----------------------------------------------------------------------------
def run_scraper(usuario, auth_token, config, ids_vistos, url):
    """
    Wrapper de compatibilidad con v1. Abre y cierra navegador por cada llamada.
    Para uso con @nayibbukele usar iniciar_sesion / scrape_url_sesion / cerrar_sesion.
    """
    return asyncio.run(
        _scrape_legacy(url, auth_token, config, set(ids_vistos))
    )


async def _scrape_legacy(url, auth_token, config, ids_vistos):
    todos_tweets = {}
    sin_cambio   = 0
    max_sin_cambio = 10

    async with async_playwright() as pw:
        navegador = await pw.chromium.launch(
            headless=config["modo_headless"],
            args=["--no-sandbox", "--disable-blink-features=AutomationControlled"]
        )
        contexto = await navegador.new_context(
            viewport={"width": 1280, "height": 900},
            user_agent=(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/120.0.0.0 Safari/537.36"
            ),
            locale="es-MX",
            timezone_id="America/Mexico_City",
        )
        await contexto.add_init_script(
            "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
        )
        await contexto.add_cookies([{
            "name": "auth_token", "value": auth_token,
            "domain": ".x.com", "path": "/"
        }])
        page = await contexto.new_page()

        try:
            await page.goto(url, wait_until="domcontentloaded", timeout=config["timeout_pagina"])
        except Exception:
            pass

        try:
            await page.wait_for_selector('article[data-testid="tweet"]', timeout=config["timeout_elemento"])
        except Exception:
            await navegador.close()
            return []

        while True:
            nuevos = await extraer_tweets_visibles(page)
            antes  = len(todos_tweets)
            for t in nuevos:
                if t["id"] and t["id"] not in todos_tweets and t["id"] not in ids_vistos:
                    todos_tweets[t["id"]] = t
            incorporados = len(todos_tweets) - antes
            if incorporados == 0:
                sin_cambio += 1
                if sin_cambio >= max_sin_cambio:
                    break
            else:
                sin_cambio = 0
            await page.evaluate(f"window.scrollBy(0, {config['scroll_pixels']})")
            await asyncio.sleep(random.uniform(config["delay_min"], config["delay_max"]))

        await navegador.close()

    return list(todos_tweets.values())
